package services

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"text/tabwriter"
	"time"

	"github.com/deploy-services/v2/log"

	// "time"

	"github.com/google/uuid"

	"github.com/deploy-services/v2/filesystem"
	"github.com/deploy-services/v2/kcfg"
)

// public {
// 	name fpbx
// 	nodes [
// 		node1
// 		node2
// 	]

//		# source 解压缩后的依赖文件，也就是进行对比的目录
//		depends [
//			fpbx/lib
//		]
//		# 压缩包也要放在 data 目录
//		# 源压缩包，必须是 gz, zip 格式，depends 的列表必须和解压缩后的路径匹配，否则会更新失败
//		# 解压缩后，如果包含 depends 下的路径，那么会进行压缩，并进行大小和MD5校验
//		source [
//			fpbx.tar.gz
//		]
//	}

func allow_source_type(file string) bool {
	switch filepath.Ext(file) {
	case ".gz":
		return true
	}
	return false
}

type source_info struct {
	File string
	Size int64
	Time string
}

type public struct {
	name          string
	nodes         []string
	server_nodes  nodes
	depends       []string
	d_info        map[string]source_info
	commit_id     string
	s_info_update bool
	d_info_update bool
	history       []histroy_info
	active_uuid   string
	after_active  cmd_batch
}

func (s *public) dump() string {
	var ss string
	ss += fmt.Sprintf("%v\n", s.name)
	ss += fmt.Sprintf("\tactive :%v\n", s.active_uuid)
	ss += fmt.Sprintf("\tnodes  :")
	for _, v := range s.nodes {
		ss += fmt.Sprintf("%v ", v)
	}
	ss += fmt.Sprintf("\n")
	ss += fmt.Sprintf("\tdepends:%v\n", len(s.d_info))
	var bs strings.Builder
	w := tabwriter.NewWriter(&bs, 0, 0, 2, ' ', 0)
	fmt.Fprintf(w, "        \tFile\tSize\tTime\n")
	fmt.Fprintf(w, "        \t----\t----\t----\n")
	for _, v := range s.d_info {
		fmt.Fprintf(w, "        \t%v\t%v\t%v\n", v.File, v.Size, v.Time)
	}
	w.Flush()
	ss += bs.String()
	return ss
}

func (s *public) show_history() string {
	var rest string

	rest += fmt.Sprintf("\n%v:\n", s.name)
	for i := len(s.history) - 1; i >= 0; i-- {
		if s.active_uuid == s.history[i].Commit {
			rest += fmt.Sprintf("\t%v\t%v\t*Active*\n", s.history[i].Time, s.history[i].Commit)
		} else {
			rest += fmt.Sprintf("\t%v\t%v\n", s.history[i].Time, s.history[i].Commit)
		}
	}
	return rest
}

func (s *public) IsExistPath(path string) bool {
	for _, v := range s.depends {
		if v == path {
			return true
		}
	}
	return false
}

func (s *public) updateNodes(m_nodes mnodes) {
	s.server_nodes = make(nodes, 0)
	for _, v := range s.nodes {
		ns, ok := m_nodes[v]
		if ok {
			s.server_nodes = append(s.server_nodes, ns...)
		}
	}
}

func (s *public) check_source(force bool) error {
	update_depends := []string{}
	for _, v := range s.depends {
		if filesystem.IsExist(filesystem.GetLocalDataFilePath(v)) {
			update_depends = append(update_depends, v)
		}
	}
	// 如果没有在解压缩的文件中找到要更新的目录，说明配置可能有问题，不需要在进行更新
	if len(update_depends) == 0 {
		return NotFound
	}
	// 将public/${public.name}.${source.name}.cache.${commit_id}/$public.source1
	// 拷贝到 public/$public.name/$public.source1
	for _, v := range update_depends {
		compress_path := filesystem.GetLocalPublicFilePath("release", v)
		filesystem.MkDirs(compress_path)
		file := filepath.Base(compress_path) + "." + s.commit_id + ".tar"
		compress_file := filesystem.GetLocalPublicFilePath("release", v, file)
		err := filesystem.Tar(compress_file, filesystem.GetLocalDataFilePath(v))
		if err != nil {
			return err
		}
		stat, err := os.Stat(compress_file)
		if err != nil {
			return err
		}
		d_info := source_info{
			File: file,
			Size: stat.Size(),
			Time: stat.ModTime().Format("2006-01-02 15:04:05"),
		}

		if !force {
			if old, ok := s.d_info[v]; ok {
				if old.Size == d_info.Size {
					log.Println(s.name, v, "Same File Size, Ignore")
					filesystem.RmFile(compress_file)
					continue
				}
			}
		}

		s.d_info[v] = d_info
		s.d_info_update = true
	}

	return nil
}

func (s *public) commit(force bool) (string, error) {
	commit_id, _ := uuid.NewUUID()
	s.commit_id = commit_id.String()
	public_base := filesystem.GetLocalPublicFilePath(s.name)
	filesystem.MkDirs(public_base)
	s.d_info_update = false
	if err := s.check_source(force); err != nil {
		log.Println(s.name, "Check", err)
		return "", err
	}
	if s.d_info_update {
		depends_path := filesystem.GetLocalPublicDependsPath(s.name, s.commit_id)
		buff, _ := json.MarshalIndent(s.d_info, "", "\t")
		ioutil.WriteFile(depends_path, buff, 0755)

		history_path := filesystem.GetLocalPublicHistoryPath(s.name)
		history := histroy_info{
			Time:   time.Now().Format("2006-01-02 15:04:05"),
			Commit: s.commit_id,
		}
		s.history = append(s.history, history)
		buff, _ = json.MarshalIndent(s.history, "", "\t")
		ioutil.WriteFile(history_path, buff, 0755)
		return s.commit_id, nil
	}
	return "", nil
}

func (s *public) get_remote_public_commit_path(base string, path string) string {
	return filesystem.GetRemotePublicCommitPath(base, path, s.d_info[path].File)
}

func (s *public) active_remote(commit_id string, depends_info map[string]source_info, n *node) error {
	handle := n.dv_handle.CreateHandle()
	if handle == nil {
		return errors.New("Connect " + n.host + " failed")
	}
	handle.EnableFileTransport()
	defer handle.Close()
	// 1. 取出远端 node  ${service}.active 文件 active 对比
	// 2. 如果相同，那么忽略
	// 3. 如果不相同，根据本地 depends  File字段查看远端是否存在对应文件
	// 4. 不存在就直接上传
	remote_active := filesystem.GetRemotePublicActivePath(n.base, s.name)
	buff, err := handle.ReadFile(remote_active)
	if err != nil {
		log.Println(s.name, n.name, commit_id, "Read File", remote_active, "Failed, Start Copy.", err)
	} else {
		if string(buff) == commit_id {
			log.Println(s.name, n.name, commit_id, "Same Commit,Skip")
			return nil
		}
	}

	for k, v := range depends_info {
		remote_commit_path := filesystem.GetRemotePublicCommitPath(n.base, k, v.File)
		exist, _ := handle.IsExist(remote_commit_path)
		if exist {
			continue
		}
		err = handle.Mkdir(remote_commit_path)
		if err != nil {
			log.Println(s.name, n.name, commit_id, "MkDir", remote_commit_path, "Failed.", err)
			return err
		}
		remote_path := filesystem.GetRemotePublicPath(n.base, "release", k)
		err = handle.Mkdir(remote_path)
		if err != nil {
			log.Println(s.name, n.name, commit_id, "MkDir", remote_path, "Failed.", err)
			return err
		}
		remote_file := filesystem.GetRemotePublicPath(n.base, "release", k, v.File)
		exist, _ = handle.IsExist(remote_file)
		if !exist {
			// 将压缩文件传输到服务器
			local_path := filesystem.GetLocalPublicFilePath("release", k, v.File)
			err = handle.PushFile(local_path, remote_file)
			if err != nil {
				log.Println(s.name, n.name, commit_id, "PushFile", local_path, "Failed.", err)
				return err
			}
		}

		// 解压缩文件到目录
		err = handle.UnTar(remote_file, remote_commit_path)
		if err != nil {
			log.Println(s.name, n.name, commit_id, "UnTar", remote_file, "Failed.", err)
			return err
		}
	}

	// 需要执行 active 后的批处理命令
	err = s.after_active.run(nil, n, handle)
	if err != nil {
		log.Println(s.name, n.name, commit_id, "After Active Run", remote_active, "Failed.", err)
		return err
	}

	err = handle.WriteFile(remote_active, []byte(commit_id))
	if err != nil {
		log.Println(s.name, n.name, commit_id, "WriteFile", remote_active, "Failed.", err)
		return err
	}
	s.active_uuid = commit_id
	log.Println(s.name, n.name, commit_id, "Active Completed")
	return nil
}

func (s *public) active(ver string) (string, error) {
	if len(s.history) == 0 {
		log.Println(s.name, "No History Info,Skip")
		return "", errors.New("Not Found Any Commit")
	}
	if ver == "latest" {
		ver = s.history[len(s.history)-1].Commit
	} else {
		find := false
		for _, v := range s.history {
			if v.Commit == ver {
				find = true
				break
			}
		}
		if !find {
			log.Println(s.name, "Not Found", ver, ",Skip")
			return "", errors.New("Commit Not Found")
		}
	}
	// 读取本地 active 依赖文件
	depends_path := filesystem.GetLocalPublicDependsPath(s.name, ver)
	buff, err := ioutil.ReadFile(depends_path)
	if err != nil {
		log.Println(s.name, "Read", depends_path, "Failed", err)
		return "", err
	}

	var depends_info map[string]source_info
	err = json.Unmarshal(buff, &depends_info)
	if err != nil {
		log.Println(s.name, "Parse", depends_path, "Failed", err)
		return "", err
	}

	for _, n := range s.server_nodes {
		err = s.active_remote(ver, depends_info, n)
		if err != nil {
			log.Println(s.name, "Active", n.name, "Failed", err)
			return "", err
		}
	}

	s.active_uuid = ver
	ioutil.WriteFile(filesystem.GetLocalPublicActivePath(s.name), []byte(ver), 0755)
	log.Println(s.name, ver, "Active Competed")
	return s.active_uuid, nil
}

func parsePublic(n *kcfg.Node) *public {
	pub := &public{
		d_info:  make(map[string]source_info),
		history: []histroy_info{},
	}
	pub.name = n.Child("name").GetString()
	nodes, _ := n.Child("nodes").ChildsAllWithoutNote()
	depends, _ := n.Child("depends").ChildsAllWithoutNote()

	for _, v := range nodes {
		pub.nodes = append(pub.nodes, v)
	}
	for _, v := range depends {
		pub.depends = append(pub.depends, v)
	}

	history_path := filesystem.GetLocalPublicHistoryPath(pub.name)
	buff, err := ioutil.ReadFile(history_path)
	if err == nil {
		json.Unmarshal(buff, &pub.history)
	}

	active_path := filesystem.GetLocalPublicActivePath(pub.name)
	buff, err = ioutil.ReadFile(active_path)
	if err == nil {
		pub.active_uuid = string(buff)
		depends_path := filesystem.GetLocalPublicDependsPath(pub.name, pub.active_uuid)
		buff, err := ioutil.ReadFile(depends_path)
		if err == nil {
			json.Unmarshal(buff, &pub.d_info)
		}
	}
	pub.after_active = cmd_batch{}
	pub.after_active.parse(n.Child("after_active"))
	return pub
}

type publics map[string]*public

func (s *publics) dump(name string) string {
	var rest string
	if len(name) == 0 {
		for _, v := range *s {
			rest += v.dump()
		}
	} else {
		for _, v := range *s {
			if v.name == name {
				rest += v.dump()
			}
		}
	}
	return rest
}

func (s *publics) updateNodes(nodes mnodes) {
	for _, v := range *s {
		v.updateNodes(nodes)
	}
}

func (s *publics) commit(name string, force bool) (names []string, rests []string, errs []error) {
	if len(name) == 0 {
		for _, v := range *s {
			rest, err := v.commit(force)
			names = append(names, v.name)
			rests = append(rests, rest)
			errs = append(errs, err)
		}
	} else {
		for _, v := range *s {
			if v.name == name {
				rest, err := v.commit(force)
				names = append(names, v.name)
				rests = append(rests, rest)
				errs = append(errs, err)
			}
		}
	}
	// return rests, errs
	return names, rests, errs
}

func (s *publics) active(name string, commit string) (names []string, rests []string, errs []error) {
	if len(name) == 0 {
		for _, v := range *s {
			rest, err := v.active(commit)
			names = append(names, v.name)
			rests = append(rests, rest)
			errs = append(errs, err)
		}
	} else {
		for _, v := range *s {
			if v.name == name {
				rest, err := v.active(commit)
				names = append(names, v.name)
				rests = append(rests, rest)
				errs = append(errs, err)
			}
		}
	}
	// return rests, errs
	return names, rests, errs
}

func (s *publics) get(name string) *public {
	for _, v := range *s {
		if v.name == name {
			return v
		}
	}
	return nil
}

func (s *publics) show_history(name string) string {
	var rest string
	if len(name) == 0 {
		for _, v := range *s {
			rest += v.show_history()
		}
	} else {
		for _, v := range *s {
			if v.name == name {
				rest += v.show_history()
			}
		}
	}
	return rest
}

func parsePublics(n []*kcfg.Node) publics {
	rest := make(publics)
	for _, nn := range n {
		sv := parsePublic(nn)
		rest[sv.name] = sv
	}
	return rest
}
